package com.opennews.openplatform.myspringbootcore.security

import org.springframework.core.env.Environment

class AuthRequestValidator(
    private val env: Environment,
    private val requestMapManager: RequestMapManager,
    private val activeTokenManager: ActiveTokenManager,
    private val tokenManager: TokenManager,
    private val apiPathPrefix: String,
) {
    /**
     * Checks access permission for the request.
     *
     * FullPath returns a matched route full path. 404 returns an empty string.
     * @param request the HttpServletRequest to be checked
     * @param response the HttpServletResponse to be returned in case of error
     * @return true if permission is granted, false otherwise
     */
    fun checkAccessPermission(
        requestMethod: String,
        requestURI: String,
        requestContextPath: String,
        authorizationHeader: String?,
    ): Boolean {
        // FullPath returns a matched route full path.
        // 404 returns an empty string.
        val urlFullPath = requestURI

        var granted = false

        if (!urlFullPath.isBlank()) {
            if (!urlFullPath.lowercase().endsWith("error")) {
                // Gets authorization from request header.
                // The string should be like below example:
                // Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU3ODIxNzUsImlhdCI6MTYwNTc3ODU3NSwiaWQiOiJ1c2VyLWlkIiwidXNlcm5hbWUiOiJIdWkiLCJyb2xlcyI6WyJBZG1pbiJdfQ.tKdhYNyeqOS_ITIV_F0sKT6N-NBFjxMkPyXAkzT43WQ

                // Validates token string to see if it is a valid access token, and it does not expire yet.
                val parsedToken = checkAuthorizationHeader(authorizationHeader)

                // If token is valid, we still need to check if it is still active.
                if (parsedToken.valid) {
                    parsedToken.valid =
                        activeTokenManager.isActiveAccessToken(
                            parsedToken.claims!!.username,
                            parsedToken.claims!!.deviceId,
                            parsedToken.token,
                        )
                }

                // If user was authenticated but token is no longer valid or user is not granted for current URL, 401 code will be sent.
                if (parsedToken.claims != null && !parsedToken.valid) {
                    granted = false
                } else {
                    // If token is valid we need to check on URL access permission.
                    granted = checkUrlOnRequestMap(requestMethod, requestContextPath, urlFullPath, parsedToken.claims)
                }
            }
        }

        return granted
    }

    /**
     * Checks the authorization header and tries to extract the claims.
     *
     * @param authorizationHeader the authorization header string, example: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU3ODIxNzUsImlhdCI6MTYwNTc3ODU3NSwiaWQiOiJ1c2VyLWlkIiwidXNlcm5hbWUiOiJIdWkiLCJyb2xlcyI6WyJBZG1pbiJdfQ.tKdhYNyeqOS_ITIV_F0sKT6N-NBFjxMkPyXAkzT43WQ
     */
    fun checkAuthorizationHeader(authorizationHeader: String?): ParsedToken {
        var result = ParsedToken()

        if (!authorizationHeader.isNullOrBlank()) {
            // Splits the string authorizationHeader around each instance of one or more consecutive white space characters.
            val fields = authorizationHeader.split("\\s".toRegex())

            // Checks if the string contains bearer and token.
            if (fields.size == 2) {
                // Gets token string.
                val token = fields[1]

                // Validates token string to see if it is a valid access token, and it does not expire yet.
                result = tokenManager.validateToken(token, env.getProperty("app.jwt.signing-key"))
                result.token = token
            }
        }

        return result
    }

    /**
     * Checks the requested URL on the security config request map.
     *
     * @param requestMethod the HTTP request method (e.g., GET, POST)
     * @param contextPath the URL root (e.g., /web-api)
     * @param url the full URL (e.g., /web-api/user/get)
     * @param claims the claims data
     * @return true if the request is granted access, false otherwise
     */
    fun checkUrlOnRequestMap(requestMethod: String, contextPath: String, url: String, claims: MyClaims?): Boolean {
        // Removes root url.
        // For example, url is /family-expense/api/user/get.
        // Url without root is /user/get.
        val urlWithoutRoot = url.replace(contextPath, "").replace("/$apiPathPrefix", "")

        var granted = false

        for (item in requestMapManager.getCachedRequestMaps()) {
            // If HTTP request method is configured, and it does not match then just ignore.
            if (!item.httpMethod.isNullOrBlank() && !requestMethod.equals(
                    item.httpMethod,
                    ignoreCase = true
                )
            ) {
                continue
            }

            // First of all we need to remove trailing wildcard.
            // For example, item.Url is /user/** or /user/*.
            // configuredUrl is /user.
            val configuredUrl = item.url.replace("/**", "").replace("/*", "")

            // Removes the configuredUrl from urlWithoutRoot.
            // Before removing, /user/get.
            // After removing, /get.
            val tempUrl = urlWithoutRoot.replace(configuredUrl, "")

            if (tempUrl.isEmpty()) {
                // If tempUrl is empty that means the url is exactly matched with configuredUrl.
                // Then we need to check on item.ConfigAttribute.
                granted = checkUrlOnConfigAttribute(claims, item.configAttribute)

                // If we find exactly match we just need to stop.
                break
            }

            if (tempUrl == urlWithoutRoot) {
                // If configuredUrl is /user/status then tempUrl will be the same as urlWithoutRoot.
                // That means a mismatch. We need to check on next.
                continue
            }

            if (!tempUrl.startsWith("/")) {
                // If tempUrl does NOT start with "/" that means the parent url does not fully match.
                // For example
                // configuredUrl: /measuringData
                // urlWithoutRoot: /measuringDataSharing/queryValidDetails
                // tempUrl will be Sharing/queryValidDetails
                continue
            }

            // Checks if url starts with tempUrl.
            // For example /authentication is configured but NOT /api/authentication.
            // We only check wildcard config if url ends with tempUrl.
            if (!urlWithoutRoot.startsWith(tempUrl)) {
                if (item.url.endsWith(RequestMapManager.URL_DOUBLE_WILDCARD)) {
                    // If tempUrl is /get or /status/check it would also match the double wildcard.
                    // Then we need to check on item.ConfigAttribute.
                    granted = checkUrlOnConfigAttribute(claims, item.configAttribute)
                } else if (item.url.endsWith(RequestMapManager.URL_SINGLE_WILDCARD) && tempUrl.contains("/")) {
                    // If tempUrl is /get it can match the single wildcard. But /status/check does NOT.
                    // Then we need to check on item.ConfigAttribute.
                    granted = checkUrlOnConfigAttribute(claims, item.configAttribute)
                }
            }
        }

        return granted
    }

    /**
     * Checks requested URL access permission on security configAttribute.
     * This would check authentication and authorization.
     */
    private fun checkUrlOnConfigAttribute(claims: MyClaims?, configAttribute: String): Boolean {
        return when {
            configAttribute.startsWith(RequestMapManager.PERMIT_ALL) -> true
            configAttribute.startsWith(RequestMapManager.IS_AUTHENTICATED_PREFIX) && claims != null -> true
            configAttribute.startsWith(RequestMapManager.HAS_ROLE_PREFIX) || configAttribute.startsWith(
                RequestMapManager.HAS_ANY_ROLE_PREFIX
            ) -> claims != null && requestMapManager.containsAnyRoleInConfigAttribute(claims.roles, configAttribute)

            configAttribute.startsWith(RequestMapManager.HAS_NO_ROLE_PREFIX) || configAttribute.startsWith(
                RequestMapManager.HAS_NO_ANY_ROLE_PREFIX
            ) -> claims != null && !requestMapManager.containsAnyRoleInConfigAttribute(claims.roles, configAttribute)

            else -> false
        }
    }
}